package org.anno.chunkdb;

import java.nio.charset.Charset;

/**
 * Mapping between cell value types and byte arrays, whose lexicographic order reflects the correct
 * ordering for the type. We encode long values with variable length as follows. 17 values of the
 * first byte indicate, absence (null), negative or positive numbers with varying counts of bytes to
 * follow. The other 239 values directly encode values -119 to 119. Therefore, 0 is encoded as 128.
 *
 * <pre>
 * 0: {@link #ABSENCE_MARKER}
 * 1: followed by 8 transformed bytes of (-119-v)
 * 2: followed by 7 transformed bytes of (-119-v)
 * ...
 * 8: followed by 1 transformed bytes of (-119-v)
 * 9: -119
 * 10: -118
 * ...
 * 127: -1
 * 128: 0
 * 129: 1
 * ...
 * 247: 119
 * 248: followed by 1 byte of v-119
 * ...
 * 255: followed by 8 bytes of v-119
 * </pre>
 */
public enum ValueType {

  STRING {
    public void decode(TypedCellVisitor visitor, byte[] buf, int offset, int len) {
      visitor.visitString(buf, offset, len);
    }
  }, DOUBLE {
    public void decode(TypedCellVisitor visitor, byte[] buf, int offset, int len) {
      if (buf[0] != ABSENCE_MARKER) {
        visitor.visitDouble(Double.longBitsToDouble(decodeLong(buf, offset, len)));
      }
    }
  }, LONG {
    public void decode(TypedCellVisitor visitor, byte[] buf, int offset, int len) {
      if (buf[0] != ABSENCE_MARKER) {
        visitor.visitLong(decodeLong(buf, offset, len));
      }
    }
  };

  /**
   * Returns encoding size of the given long value, which is in the range of 1 through 9.
   */
  public static int getSerializedSize(long v) {
    v = v < 0 ? -v - 119 : v - 119;
    int size = 1;
    for (; v > 0; v >>>= 8) {
      size++;
    }
    return size;
  }

  /**
   * Returns the given string encoded using UTF-8.
   */
  public static byte[] encodeForOrder(String s) {
    return s.getBytes(UTF8);
  }

  /**
   * Encode with variable length, mapping order to lexicographic order on unsigned bytes.
   */
  public static byte[] encodeForOrder(long v) {
    int size = getSerializedSize(v);
    byte[] result = new byte[size];
    write(v, result, 0, size);
    return result;
  }

  /**
   * Writes the encoded bytes into the given buffer starting at the given offset and returns the
   * count of bytes written.
   */
  public static int encodeForOrder(long v, byte[] buf, int offset) {
    return write(v, buf, offset, getSerializedSize(v));
  }

  /**
   * Writes a marker to indicate the absence of a typed value and returns the count of bytes
   * written.
   */
  public static int encodeAbsentTypeValue(byte[] buf, int offset) {
    buf[offset] = ABSENCE_MARKER;
    return 1;
  }

  /**
   * Calls at most one of {@link TypedCellVisitor#visitString}, {@link TypedCellVisitor#visitLong},
   * or {@link TypedCellVisitor#visitDouble} with the decoded value from the {@code len} bytes
   * starting at {@code offset}.
   */
  public abstract void decode(TypedCellVisitor visitor, byte[] buf, int offset, int len);

  private static int write(long v, byte[] buf, int offset, int len) {
    if (v < -119) {
      v = -119 - v;
      buf[offset] = (byte) (10 - len);
      for (int i = len; --i > 0; v >>= 8) {
        // transform bytes of negative numbers for unsigned byte comparison
        buf[offset + i] = (byte) (255 - (v & 255));
      }
    } else if (v <= 119) {
      buf[offset] = (byte) (v + 128);
    } else {
      v -= 119;
      buf[offset] = (byte) (246 + len);
      for (int i = len; --i > 0; v >>= 8) {
        buf[offset + i] = (byte) v;
      }
    }
    return len;
  }

  private static long decodeLong(byte[] buf, int offset, int len) {
    int header = buf[0] & 255;
    if (header < 9) {
      // negative numbers less than -119
      long v = 0;
      for (int i = offset + 1; i < offset + len; i++) {
        v = (v << 8) + (255 - (buf[i] & 255));
      }
      return -119 - v;
    } else if (len == 1) {
      return header - 128;
    }
    long v = 0;
    for (int i = offset + 1; i < offset + len; i++) {
      v = (v << 8) + (buf[i] & 255);
    }
    return v + 119;
  }

  private static final Charset UTF8 = Charset.forName("UTF-8");
  private static final int ABSENCE_MARKER = 0;
}
